package org.beynet.jsonbinding.bind;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class JSONBMarshaller {
    
    JSONBMarshaller(JSONBContext context,String targetEncoding) {
        this.context = context ;
        this.defaultEncoding = targetEncoding;
    }
    
    
    private void writeStringToStream(String st,OutputStream os,String encoding) throws IOException {
        os.write(st.getBytes(encoding));
    }
    
    /**
     * marshall in the default encoding in the provided stream
     * @param b : object to marshall
     * @param os
     * @throws IOException
     * @throws IllegalArgumentException : if the class of object b is not in the scope of the underlying context
     */
    public void marshall(Object b, OutputStream os) throws IOException, IllegalArgumentException {
        JSONBClassStructure st = context.getClassInScopeStructure(b.getClass());
        if (st==null) throw new IllegalArgumentException("class "+b.getClass().getCanonicalName()+" not in current context scope");
        writeStringToStream("{\n",os,defaultEncoding);
        marshall(b,st.getExpectedElementName(), os,0,defaultEncoding);
        writeStringToStream("}",os,defaultEncoding);
    }
    
    private void marshall(Object b,String name,OutputStream os,int level,String encoding) throws UnsupportedEncodingException, IOException {
        JSONBClassStructure st = context.getClassInScopeStructure(b.getClass());
        addSpaces(level,os,encoding);
        if (name!=null) {
            writeStringToStream("\"",os,encoding);
            writeStringToStream(name,os,encoding);
            writeStringToStream("\" : ",os,encoding);
        }
        if (st!=null) {
            marshallObject(b,st,os,level,encoding);
        }
        else {
            mashallType(b,os,level,encoding);
        }
       
    }

    private void mashallType(Object b, OutputStream os, int level,String encoding) throws IOException {
        if (b instanceof String) {
            writeStringToStream("\"",os,encoding);
            writeStringToStream((String)b,os,encoding);
            writeStringToStream("\"",os,encoding);
        }
        else if (b instanceof BigDecimal) {
            writeStringToStream(b.toString(),os,encoding);
        }
        else if (b instanceof Boolean) {
            writeStringToStream(b.toString(),os,encoding);
        }
        else if (b instanceof List) {
            List<?> list = (List<?>)b;
            writeStringToStream("[",os,encoding);
            boolean first = true;
            for (Object o :list) {
                if (first==false) writeStringToStream(",",os,encoding);
                marshall(o, null, os, level+1, encoding);
                first = false;
            }
            writeStringToStream("]",os,encoding);
        }
        else {
            throw new RuntimeException("type "+b.getClass().getCanonicalName()+" not handled yet");
        }
    }

    private void addSpaces(int level,OutputStream os,String encoding) throws UnsupportedEncodingException, IOException {
        for (int i=0;i<level;i++) {
            writeStringToStream("\t",os,encoding);
        }
    }
    
    private void marshallObject(Object b, JSONBClassStructure st, OutputStream os, int level,String encoding) throws UnsupportedEncodingException, IOException {
        Iterator<Map<Method,String>> methods = st.getMethods();
        boolean first = true;
        addSpaces(level,os,encoding);
        writeStringToStream("{\n",os,encoding);
        while (methods.hasNext()) {
            if (first==false) {
                writeStringToStream(" , \n",os,encoding);
            }
            Map<Method, String> next = methods.next();
            Entry<Method, String> entry = next.entrySet().iterator().next();
            Method method = entry.getKey();
            String name= entry.getValue();
            try {
                Object resultObj = method.invoke(b);
                if (resultObj!=null) {
                    marshall(resultObj, name, os, level+1,encoding);
                    if (first==true) {
                        first = false ;
                    }
                }
            } catch (Exception e) {
               throw new RuntimeException("error invoking annotated method "+method.getName()+" on "+b.getClass().getCanonicalName(),e);
            }
        }
        addSpaces(level,os,encoding);
        writeStringToStream("}\n",os,encoding);
    }
    
    private JSONBContext context;
    
    private String defaultEncoding ;
}
